{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Application - Example 2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Suppose we have a `Polygon` class that has a vertices property that needs to be defined as a sequence of `Point2D` instances. So here, not only do we want the `vertices` attribute of our `Polygon` to be an iterable of some kind, we also want the elements to all be instances of the `Point2D` class. In turn we'll also want to make sure that coordinates for `Point2D` are non-negative integer values (as might be expected in computer screen coordinates):"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's start by defining the `Point2D` class, but we'll need a descriptor for the coordinates to ensure they are integer values, possibly bounded between min and max values:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Int:\n",
    "    def __init__(self, min_value=None, max_value=None):\n",
    "        self.min_value = min_value\n",
    "        self.max_value = max_value\n",
    "        \n",
    "    def __set_name__(self, owner_class, name):\n",
    "        self.name = name\n",
    "        \n",
    "    def __set__(self, instance, value):\n",
    "        if not isinstance(value, int):\n",
    "            raise ValueError(f'{self.name} must be an int.')\n",
    "        if self.min_value is not None and value < self.min_value:\n",
    "            raise ValueError(f'{self.name} must be at least {self.min_value}')\n",
    "        if self.max_value is not None and value > self.max_value:\n",
    "            raise ValueError(f'{self.name} cannot exceed {self.max_value}')\n",
    "        instance.__dict__[self.name] = value\n",
    "        \n",
    "    def __get__(self, instance, owner_class):\n",
    "        if instance is None:\n",
    "            return self\n",
    "        else:\n",
    "            return instance.__dict__.get(self.name, None)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Point2D:\n",
    "    x = Int(min_value=0, max_value=800)\n",
    "    y = Int(min_value=0, max_value=400)\n",
    "    \n",
    "    def __init__(self, x, y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "        \n",
    "    def __repr__(self):\n",
    "        return f'Point2D(x={self.x}, y={self.y})'\n",
    "    \n",
    "    def __str__(self):\n",
    "        return f'({self.x}, {self.y})'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And our `Point2D` class will now only allow integer values within the defined range:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Point2D(0, 10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'(0, 10)'"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "str(p)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Point2D(x=0, y=10)'"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "repr(p)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(0, 10)"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.x, p.y"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "y cannot exceed 400\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    p = Point2D(0, 500)\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next let's create a validator that checks that we have a sequence (mutable or immutable, does not matter) of `Point2D` objects. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To check of something is a sequence, we can use the abstract base classes defined in the `collections` module:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "import collections"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "isinstance([1, 2, 3], collections.abc.Sequence)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "isinstance([1, 2, 3], collections.abc.MutableSequence)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "isinstance((1, 2, 3), collections.abc.Sequence)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "isinstance((1, 2, 3), collections.abc.MutableSequence)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So let's write the validator:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Point2DSequence:\n",
    "    def __init__(self, min_length=None, max_length=None):\n",
    "        self.min_length = min_length\n",
    "        self.max_length = max_length\n",
    "        \n",
    "    def __set_name__(self, cls, name):\n",
    "        self.name = name\n",
    "        \n",
    "    def __set__(self, instance, value):\n",
    "        if not isinstance(value, collections.abc.Sequence):\n",
    "            raise ValueError(f'{self.name} must be a sequence type.')\n",
    "        if self.min_length is not None and len(value) < self.min_length:\n",
    "            raise ValueError(f'{self.name} must contain at least '\n",
    "                             f'{self.min_length} elements'\n",
    "                            )\n",
    "        if self.max_length is not None and len(value) > self.max_length:\n",
    "            raise ValueError(f'{self.name} cannot contain more than  '\n",
    "                             f'{self.max_length} elements'\n",
    "                            )\n",
    "        for index, item in enumerate(value):\n",
    "            if not isinstance(item, Point2D):\n",
    "                raise ValueError(f'Item at index {index} is not a Point2D instance.')\n",
    "                \n",
    "        # value passes checks - want to store it as a mutable sequence so we can \n",
    "        # append to it later\n",
    "        instance.__dict__[self.name] = list(value)\n",
    "        \n",
    "    def __get__(self, instance, cls):\n",
    "        if instance is None:\n",
    "            return self\n",
    "        else:\n",
    "            if self.name not in instance.__dict__:\n",
    "                # current point list has not been defined,\n",
    "                # so let's create an empty list\n",
    "                instance.__dict__[self.name] = []\n",
    "            return instance.__dict__.get(self.name)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And now we can use this for our `Polygon` class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Polygon:\n",
    "    vertices = Point2DSequence(min_length=3)\n",
    "    \n",
    "    def __init__(self, *vertices):\n",
    "        self.vertices = vertices"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "vertices must contain at least 3 elements\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    p = Polygon()\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "x must be at least 0\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    p = Polygon(Point2D(-100,0), Point2D(0, 1), Point2D(1, 0))\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Polygon(Point2D(0,0), Point2D(0, 1), Point2D(1, 0))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[Point2D(x=0, y=0), Point2D(x=0, y=1), Point2D(x=1, y=0)]"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.vertices"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "OK, so, for completeness, let's write a method that we can use to append new points to the vertices list (that's why we made it a mutable sequence type!)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Polygon:\n",
    "    vertices = Point2DSequence(min_length=3)\n",
    "    \n",
    "    def __init__(self, *vertices):\n",
    "        self.vertices = vertices\n",
    "        \n",
    "    def append(self, pt):\n",
    "        if not isinstance(pt, Point2D):\n",
    "            raise ValueError('Can only append Point2D instances.')\n",
    "        max_length = type(self).vertices.max_length\n",
    "        if max_length is not None and len(self.vertices) >= max_length:\n",
    "            # cannot add more points!\n",
    "            raise ValueError(f'Vertices length is at max ({max_length})')\n",
    "        self.vertices.append(pt)\n",
    "                "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Polygon(Point2D(0,0), Point2D(1,0), Point2D(0,1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[Point2D(x=0, y=0), Point2D(x=1, y=0), Point2D(x=0, y=1)]"
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.vertices"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [],
   "source": [
    "p.append(Point2D(10, 10))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[Point2D(x=0, y=0), Point2D(x=1, y=0), Point2D(x=0, y=1), Point2D(x=10, y=10)]"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.vertices"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we could set a `max_length` directly when we define the `Polygon` class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Polygon:\n",
    "    vertices = Point2DSequence(min_length=3, max_length=3)\n",
    "    \n",
    "    def __init__(self, *vertices):\n",
    "        self.vertices = vertices\n",
    "        \n",
    "    def append(self, pt):\n",
    "        if not isinstance(pt, Point2D):\n",
    "            raise ValueError('Can only append Point2D instances.')\n",
    "        max_length = type(self).vertices.max_length\n",
    "        if max_length is not None and len(self.vertices) >= max_length:\n",
    "            # cannot add more points!\n",
    "            raise ValueError(f'Vertices length is at max ({max_length})')\n",
    "        self.vertices.append(pt)\n",
    "                "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Polygon(Point2D(0,0), Point2D(1,0), Point2D(0,1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Vertices length is at max (3)\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    p.append(Point2D(10, 10))\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But instead, let's use inheritance to create special `Polygon` types!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "First let's go back to our original `Polygon` definition:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Polygon:\n",
    "    vertices = Point2DSequence(min_length=3)\n",
    "    \n",
    "    def __init__(self, *vertices):\n",
    "        self.vertices = vertices\n",
    "        \n",
    "    def append(self, pt):\n",
    "        if not isinstance(pt, Point2D):\n",
    "            raise ValueError('Can only append Point2D instances.')\n",
    "        max_length = type(self).vertices.max_length\n",
    "        if max_length is not None and len(self.vertices) >= max_length:\n",
    "            # cannot add more points!\n",
    "            raise ValueError(f'Vertices length is at max ({max_length})')\n",
    "        self.vertices.append(pt)\n",
    "                "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Triangle(Polygon):\n",
    "    vertices = Point2DSequence(min_length=3, max_length=3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So `Triangle` redefines the vertices property, but inherits both the `__init__` and `append` methods:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Polygon(Point2D(0,0), Point2D(1,0), Point2D(0,1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [],
   "source": [
    "p.append(Point2D(10, 10))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[Point2D(x=0, y=0), Point2D(x=1, y=0), Point2D(x=0, y=1), Point2D(x=10, y=10)]"
      ]
     },
     "execution_count": 31,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.vertices"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "That works fine, but this does not:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = Triangle(Point2D(0,0), Point2D(1,0), Point2D(0,1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Vertices length is at max (3)\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    t.append(Point2D(10, 10))\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And we can also do a square:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Square(Polygon):\n",
    "    vertices = Point2DSequence(min_length=4, max_length=4)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = Square(Point2D(0,0), Point2D(1,0), Point2D(0,1), Point2D(1, 1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[Point2D(x=0, y=0), Point2D(x=1, y=0), Point2D(x=0, y=1), Point2D(x=1, y=1)]"
      ]
     },
     "execution_count": 36,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s.vertices"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Vertices length is at max (4)\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    s.append(Point2D(10, 10))\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We could actually improve this even more by making our `Polygon` class an actual sequence type. To do that we only need to implement a few special methods:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Polygon:\n",
    "    vertices = Point2DSequence(min_length=3)\n",
    "    \n",
    "    def __init__(self, *vertices):\n",
    "        self.vertices = vertices\n",
    "        \n",
    "    def append(self, pt):\n",
    "        if not isinstance(pt, Point2D):\n",
    "            raise ValueError('Can only append Point2D instances.')\n",
    "        max_length = type(self).vertices.max_length\n",
    "        if max_length is not None and len(self.vertices) >= max_length:\n",
    "            # cannot add more points!\n",
    "            raise ValueError(f'Vertices length is at max ({max_length})')\n",
    "        self.vertices.append(pt)\n",
    "                \n",
    "    def __len__(self):\n",
    "        return len(self.vertices)\n",
    "        \n",
    "    def __getitem__(self, idx):\n",
    "        return self.vertices[idx]\n",
    "        "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Polygon(Point2D(0,0), Point2D(1,0), Point2D(1,1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "3"
      ]
     },
     "execution_count": 40,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "len(p)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[Point2D(x=0, y=0), Point2D(x=1, y=0), Point2D(x=1, y=1)]"
      ]
     },
     "execution_count": 41,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "list(p)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(Point2D(x=0, y=0), Point2D(x=1, y=0), Point2D(x=1, y=1))"
      ]
     },
     "execution_count": 42,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p[0], p[1], p[2]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[Point2D(x=0, y=0), Point2D(x=1, y=0)]"
      ]
     },
     "execution_count": 43,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p[0:2]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We could even implement in-place addition and containment:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Polygon:\n",
    "    vertices = Point2DSequence(min_length=3)\n",
    "    \n",
    "    def __init__(self, *vertices):\n",
    "        self.vertices = vertices\n",
    "        \n",
    "    def append(self, pt):\n",
    "        if not isinstance(pt, Point2D):\n",
    "            raise ValueError('Can only append Point2D instances.')\n",
    "        max_length = type(self).vertices.max_length\n",
    "        if max_length is not None and len(self.vertices) >= max_length:\n",
    "            # cannot add more points!\n",
    "            raise ValueError(f'Vertices length is at max ({max_length})')\n",
    "        self.vertices.append(pt)\n",
    "                \n",
    "    def __len__(self):\n",
    "        return len(self.vertices)\n",
    "        \n",
    "    def __getitem__(self, idx):\n",
    "        return self.vertices[idx]\n",
    "        \n",
    "    def __iadd__(self, pt):\n",
    "        self.append(pt)\n",
    "        return self\n",
    "    \n",
    "    def __contains__(self, pt):\n",
    "        return pt in self.vertices"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Polygon(Point2D(0,0), Point2D(1,0), Point2D(1,1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[Point2D(x=0, y=0), Point2D(x=1, y=0), Point2D(x=1, y=1)]"
      ]
     },
     "execution_count": 46,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "list(p)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "metadata": {},
   "outputs": [],
   "source": [
    "p += Point2D(10, 10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[Point2D(x=0, y=0), Point2D(x=1, y=0), Point2D(x=1, y=1), Point2D(x=10, y=10)]"
      ]
     },
     "execution_count": 48,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "list(p)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "What about containment?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 49,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Point2D(0, 0) in p"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Why `False`? The point (0,0) is in the vertices list... "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Well, we didn't override the `__eq__` method in our `Point2D` class, so it's using the implementation in `object`, which uses object identity.\n",
    "\n",
    "We can easily fix that:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Point2D:\n",
    "    x = Int(min_value=0, max_value=800)\n",
    "    y = Int(min_value=0, max_value=400)\n",
    "    \n",
    "    def __init__(self, x, y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "        \n",
    "    def __repr__(self):\n",
    "        return f'Point2D(x={self.x}, y={self.y})'\n",
    "    \n",
    "    def __str__(self):\n",
    "        return f'({self.x}, {self.y})'\n",
    "    \n",
    "    def __eq__(self, other):\n",
    "        return isinstance(other, Point2D) and self.x == other.x and self.y == other.y\n",
    "        \n",
    "    def __hash__(self):\n",
    "        return hash((self.x, self.y))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Polygon:\n",
    "    vertices = Point2DSequence(min_length=3)\n",
    "    \n",
    "    def __init__(self, *vertices):\n",
    "        self.vertices = vertices\n",
    "        \n",
    "    def append(self, pt):\n",
    "        if not isinstance(pt, Point2D):\n",
    "            raise ValueError('Can only append Point2D instances.')\n",
    "        max_length = type(self).vertices.max_length\n",
    "        if max_length is not None and len(self.vertices) >= max_length:\n",
    "            # cannot add more points!\n",
    "            raise ValueError(f'Vertices length is at max ({max_length})')\n",
    "        self.vertices.append(pt)\n",
    "                \n",
    "    def __len__(self):\n",
    "        return len(self.vertices)\n",
    "        \n",
    "    def __getitem__(self, idx):\n",
    "        return self.vertices[idx]\n",
    "        \n",
    "    def __iadd__(self, pt):\n",
    "        self.append(pt)\n",
    "        return self\n",
    "    \n",
    "    def __contains__(self, pt):\n",
    "        return pt in self.vertices"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Polygon(Point2D(0,0), Point2D(1,0), Point2D(1,1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 53,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Point2D(0,0) in p"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
